# frozen_string_literal: true

# Copyright 2015-2017, the Linux Foundation, IDA, and the
# OpenSSF Best Practices badge contributors
# SPDX-License-Identifier: MIT

require_relative '../lib/locale_utils'

# rubocop:disable Metrics/BlockLength

# The priority is based upon order of creation:
# first created -> highest priority.
# See how all your routes lay out with "rake routes".

# This regex defines all legal locale values:
LEGAL_LOCALE ||= /(?:#{I18n.available_locales.join('|')})/

# Canonical lists of valid criteria level names are defined in:
# config/initializers/00_criteria_levels.rb
# This includes: METAL_LEVEL_NAMES, METAL_LEVEL_NUMBERS, BASELINE_LEVEL_NAMES,
# LEVEL_SYNONYMS, SPECIAL_FORMS, and ALL_CRITERIA_LEVEL_NAMES

# This regex is used to verify criteria levels in routes:
# Built from ALL_CRITERIA_LEVEL_NAMES - DO NOT edit this directly
# To add new levels, update config/initializers/00_criteria_levels.rb
VALID_CRITERIA_LEVEL ||= /#{Regexp.union(ALL_CRITERIA_LEVEL_NAMES)}/

# Confirm that number-only id is provided
VALID_ID ||= /[1-9][0-9]*/

# Valid values for static badge display
VALID_STATIC_VALUE ||= /0|[1-9]{1,2}|passing|silver|gold/

# Map of old (deprecated) criteria levels to new (canonical) levels
# Used to generate redirect routes automatically (DRY)
LEVEL_REDIRECTS = {
  '0' => 'passing',
  '1' => 'silver',
  '2' => 'gold',
  'bronze' => 'passing'
}.freeze

# Simplified redirect helper for project routes
# Detects locale from params or request, builds redirect path
# When locale is in params: returns 301 permanent redirect (cacheable)
# When locale is not in params: returns 302 temporary (varies by Accept-Language)
def redirect_to_level(new_level, suffix: '', status: 302)
  redirect(status: status) do |params, req|
    locale = params[:locale] || LocaleUtils.find_best_locale(req).to_s
    format_suffix = params[:format] ? ".#{params[:format]}" : ''
    "/#{locale}/projects/#{params[:id]}/#{new_level}#{suffix}#{format_suffix}"
  end
end

Rails.application.routes.draw do
  # First, handle routing of special cases.
  # Warning: Routes that don't take a :locale value must include a
  # "skip_before_action :redir_missing_locale ..." in their controller.

  # The "robots.txt" file is always at the root of the
  # document tree and has no locale. Handle it specially.
  get '/robots.txt' => 'static_pages#robots',
      defaults: { format: 'text' }, as: :robots

  # The /projects/NUMBER/badge image route needs speed and never uses a
  # locale. Perhaps most importantly, badge images need to have
  # a single canonical name so that the CDN caches will work correctly.
  # If we use a single canonical name for a badge image, we can then
  # change or invalidate a single CDN cache to update a badge image.
  # If we had different routes for each locale, there'd be much
  # more work to change or invalidate them, with no purpose.
  # Therefore, instead of redirecting the badge image to a locale if
  # one is not listed, we do *NOT* support locale URLs in this case.
  get '/projects/:id/badge' => 'projects#badge',
      constraints: { id: VALID_ID },
      defaults: { format: 'svg' }

  # The /badge_static/:value route needs speed and never uses a locale.
  # Beware: This route produces a result unconnected to a project's status.
  # Do NOT use this route on a project's README.md page!
  get '/badge_static/:value' => 'badge_static#show',
      constraints: { value: VALID_STATIC_VALUE },
      defaults: { format: 'svg' }

  # These routes never use locales, so that the cache is shared across locales.
  get '/project_stats/total_projects', to: 'project_stats#total_projects',
    as: 'total_projects_project_stats',
    constraints: ->(req) { req.format == :json }
  get '/project_stats/nontrivial_projects',
      to: 'project_stats#nontrivial_projects',
      as: 'nontrivial_projects_project_stats',
      constraints: ->(req) { req.format == :json }
  get '/project_stats/silver', to: 'project_stats#silver',
    as: 'silver_project_stats',
    constraints: ->(req) { req.format == :json }
  get '/project_stats/gold', to: 'project_stats#gold',
    as: 'gold_project_stats',
    constraints: ->(req) { req.format == :json }

  # Weird special case: for David A. Wheeler to get log issues from Google,
  # we have to let Google verify this.  Locale is irrelevant.
  # It isn't really HTML, even though the filename extension is .html. See:
  # https://github.com/coreinfrastructure/best-practices-badge/issues/1223
  get '/google75f94b1182a77eb8.html' => 'static_pages#google_verifier',
      defaults: { format: 'text' }

  # Now handle the normal case: routes with an optional locale prefix.
  # We include almost all routes inside a :locale header,
  # where the locale is optional.  This approach (using an optional value)
  # is easier to use in Rails than some alternatives.  For example,
  # by doing this, helpers like root_path accept a locale parameter but work
  # without one (as expected).
  # If a locale is not provided, the ApplicationController will normally
  # send the web browser a redirect to the best locale it can identify.
  scope '(:locale)', locale: LEGAL_LOCALE do
    # TODO: Force a canonical URL for the top (root) level (with or without
    # a trailing slash), by redirecting the other.
    # For now, we just accept either.
    # The system itself always generates root URLs *without* a trailing slash.
    root to: 'static_pages#home'

    get '/project_stats', to: 'project_stats#index', as: 'project_stats'
    get '/project_stats/activity_30', to: 'project_stats#activity_30',
      as: 'activity_30_project_stats',
      constraints: ->(req) { req.format == :json }
    get '/project_stats/daily_activity', to: 'project_stats#daily_activity',
      as: 'daily_activity_project_stats',
      constraints: ->(req) { req.format == :json }
    get '/project_stats/reminders', to: 'project_stats#reminders',
      as: 'reminders_project_stats',
      constraints: ->(req) { req.format == :json }
    get '/project_stats/silver_and_gold', to: 'project_stats#silver_and_gold',
      as: 'silver_and_gold_project_stats',
      constraints: ->(req) { req.format == :json }
    get '/project_stats/percent_earning', to: 'project_stats#percent_earning',
      as: 'percent_earning_project_stats',
      constraints: ->(req) { req.format == :json }
    get '/project_stats/user_statistics', to: 'project_stats#user_statistics',
      as: 'user_statistics_project_stats',
      constraints: ->(req) { req.format == :json }
    # The following route isn't very useful; we may remove it in the future:
    get '/project_stats/:id', to: 'project_stats#show',
        constraints: { id: VALID_ID }

    get 'sessions/new'

    get 'signup' => 'users#new'

    # Handle "static" pages (get-only pages)
    get 'home' => 'static_pages#home'
    get 'criteria_stats' => 'static_pages#criteria_stats'
    get 'criteria_discussion' => 'static_pages#criteria_discussion'
    get 'cookies' => 'static_pages#cookies'

    get 'feed' => 'projects#feed', defaults: { format: 'atom' }
    get 'reminders' => 'projects#reminders_summary'

    # PERFORMANCE NOTE: These route-level redirects are processed early in the request
    # cycle, before controllers/models are loaded, using minimal memory. They return
    # redirect responses directly from the routing layer.

    # Redirects for deprecated level names/numbers to canonical names
    # Single-hop redirects: 0 → passing (not 0 → bronze → passing)
    # With locale: 301 permanent (cacheable), without locale: 302 temporary (varies by user)

    LEVEL_REDIRECTS.each do |old_level, new_level|
      # Show routes: /projects/:id/:old_level -> /projects/:id/:new_level
      get "/:locale/projects/:id/#{old_level}(.:format)",
          to: redirect(301) { |p, _|
            fmt = p[:format] ? ".#{p[:format]}" : ''
            "/#{p[:locale]}/projects/#{p[:id]}/#{new_level}#{fmt}"
          },
          constraints: { id: VALID_ID, locale: LEGAL_LOCALE }

      get "/projects/:id/#{old_level}(.:format)",
          to: redirect_to_level(new_level, status: 302),
          constraints: { id: VALID_ID }

      # Edit routes: /projects/:id/:old_level/edit -> /projects/:id/:new_level/edit
      get "/:locale/projects/:id/#{old_level}/edit(.:format)",
          to: redirect(301) { |p, _|
            fmt = p[:format] ? ".#{p[:format]}" : ''
            "/#{p[:locale]}/projects/#{p[:id]}/#{new_level}/edit#{fmt}"
          },
          constraints: { id: VALID_ID, locale: LEGAL_LOCALE }

      get "/projects/:id/#{old_level}/edit(.:format)",
          to: redirect_to_level(new_level, suffix: '/edit', status: 302),
          constraints: { id: VALID_ID }
    end

    # Redirect routes without criteria_level to /passing (default)
    # Always use temporary redirect (302) since default may change per-project in future
    # Must come BEFORE resources :projects to take precedence over default routes
    # All criteria_level queries (well-formed and malformed) handled by controller

    # Show routes: Exclude JSON and MD formats (handled specially in resources)
    get '/:locale/projects/:id(.:format)',
        to: redirect_to_level('passing', status: 302),
        constraints: lambda { |req|
          # Check ID is numeric
          id_ok = req.params[:id]&.match?(/\A#{VALID_ID.source}\z/)
          # Check locale is valid
          locale_ok = req.params[:locale]&.match?(/\A#{LEGAL_LOCALE.source}\z/)
          # Exclude json and md formats
          format_ok = !%i[json md].include?(req.format.to_sym)
          # Don't match any criteria_level queries - let controller handle those
          no_criteria_level = !req.query_string.include?('criteria_level')
          id_ok && locale_ok && format_ok && no_criteria_level
        }

    get '/projects/:id(.:format)',
        to: redirect_to_level('passing', status: 302),
        constraints: lambda { |req|
          # Check ID is numeric
          id_ok = req.params[:id]&.match?(/\A#{VALID_ID.source}\z/)
          # Exclude json and md formats
          format_ok = !%i[json md].include?(req.format.to_sym)
          # Don't match any criteria_level queries - let controller handle those
          no_criteria_level = !req.query_string.include?('criteria_level')
          id_ok && format_ok && no_criteria_level
        }

    # Edit routes: All formats allowed (no malformed edit queries expected)
    get '/:locale/projects/:id/edit(.:format)',
        to: redirect_to_level('passing', suffix: '/edit', status: 302),
        constraints: { id: VALID_ID, locale: LEGAL_LOCALE }

    get '/projects/:id/edit(.:format)',
        to: redirect_to_level('passing', suffix: '/edit', status: 302),
        constraints: { id: VALID_ID }

    resources :projects, constraints: { id: VALID_ID } do
      member do
        get 'delete_form' => 'projects#delete_form'
        get '' => 'projects#show_json',
            constraints: ->(req) { req.format == :json }
        get '' => 'projects#show_markdown',
            constraints: ->(req) { req.format == :md }
        get ':criteria_level(.:format)' => 'projects#show',
            constraints: { criteria_level: VALID_CRITERIA_LEVEL },
            as: :level
        get ':criteria_level/edit(.:format)' => 'projects#edit',
            constraints: { criteria_level: VALID_CRITERIA_LEVEL },
            as: :level_edit
      end
    end
    match(
      'projects/:id/(:criteria_level/)edit' => 'projects#update',
      via: %i[put patch], as: :put_project,
      constraints: { criteria_level: VALID_CRITERIA_LEVEL }
    )

    resources :users
    resources :account_activations, only: [:edit]
    resources :password_resets,     only: %i[new create edit update]

    get 'criteria/:criteria_level', to: 'criteria#show'
    get 'criteria', to: 'criteria#index'

    get 'login' => 'sessions#new'
    post 'login' => 'sessions#create'
    get 'auth/:provider/callback' => 'sessions#create'
    get '/signout' => 'sessions#destroy', as: :signout
    delete 'logout' => 'sessions#destroy'

    get 'unsubscribe' => 'unsubscribe#edit'
    post 'unsubscribe' => 'unsubscribe#create'

    # No other route, send a 404 ("not found").
    match '*path', via: :all, to: 'static_pages#error_404'
  end

  # No other route ("locale" wasn't allowed), so send a 404 ("not found").
  match '*path', via: :all, to: 'static_pages#error_404'

  # Here are some examples of routes.

  # Example of regular route:
  #   get 'products/:id' => 'catalog#view'

  # Example of named route that can be invoked with purchase_url(id: product.id)
  #   get 'products/:id/purchase' => 'catalog#purchase', as: :purchase

  # Example resource route (maps HTTP verbs to controller actions
  # automatically):
  #   resources :products

  # Example resource route with options:
  #   resources :products do
  #     member do
  #       get 'short'
  #       post 'toggle'
  #     end
  #
  #     collection do
  #       get 'sold'
  #     end
  #   end

  # Example resource route with sub-resources:
  #   resources :products do
  #     resources :comments, :sales
  #     resource :seller
  #   end

  # Example resource route with more complex sub-resources:
  #   resources :products do
  #     resources :comments
  #     resources :sales do
  #       get 'recent', on: :collection
  #     end
  #   end

  # Example resource route with concerns:
  #   concern :toggleable do
  #     post 'toggle'
  #   end
  #   resources :posts, concerns: :toggleable
  #   resources :photos, concerns: :toggleable

  # Example resource route within a namespace:
  #   namespace :admin do
  #     # Directs /admin/products/* to Admin::ProductsController
  #     # (app/controllers/admin/products_controller.rb)
  #     resources :products
  #   end
end
# rubocop:enable Metrics/BlockLength
